モバイルアプリのユーザーエージェント設計のベストプラクティス
ユーザーエージェントとは?
ユーザーエージェント(User Agent)は、HTTPリクエストを行う際にリクエストを行う元(クライアント)の種類を特定するものです。
ユーザーエージェント (User agent)とは、利用者があるプロトコルに基づいてデータを利用する際に用いるソフトウェアまたはハードウェアのこと。 特にHTTPを用いてWorld Wide Webにアクセスする、ウェブブラウザなどのソフトウェアのこと。
モバイルアプリではAPIリクエストを行う際、HTTPリクエストヘッダの User-Agent
を使ってユーザーエージェントの情報を送信します。ユーザーエージェントの情報は任意の文字列となりますが、特に決まりがないため、何をどのように設定すれば良いか迷ってしまいます。
そこで本記事では、ユーザーエージェントが果たす目的を考えつつユーザーエージェント設計のベストプラクティスを考察してみたいと思います。
デフォルト値
デフォルト値は、それぞれ下記の通りになっています (${}
は変数を表します)。
iOSのNSURLConnection
${CFBundleName}/${CFBundleVersion} CFNetwork/672.0.8 Darwin/14.0.0
iOSのWeb View
Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.32 (KHTML, like Gecko) Mobile/13A4254v
AndroidのHttpURLConnection
Dalvik/1.4.0 (Linux; U; Android 2.3.2; Nexus S Build/GRH78C)
AndroidのWeb View
Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/KRT16M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36
OSSなどのラッパーを使った場合
OSSなどラッパーを使っている場合、独自の形式に書き換えられていることが多いです。
iOSのAlamofireの場合
- ソースコードはこちらを参照(※バージョン4.5.1時点)
iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0
のような形式となるNSURLConnection
のフォーマットと異なる- iOSであることが判別しづらい
- App IDやBundle Identifier依存になっており、環境ごとにBundle Identifierを分けている場合は共通化できていない
AndroidのOkHTTPの場合
- ソースコードはこちらを参照(※バージョン3.9.0時点)
- 例
okhttp/3.2.0
のような形式となる HttpURLConnection
のフォーマットと異なる- 得られる情報が少なすぎるので「あくまで仮決めだから変えてね」というスタンスが見受けられる
そのまま使う場合のデメリット
ユーザーエージェントが適切に設定されていないと、以下のような問題に遭遇します。
- アクセスログで、リクエスト数などのメトリクスが正確に測れない
- 得られる情報が少ないと正規表現でノイズを弾ききれない場合がある
- 偽装された瞬間に破綻する
- 将来的に WAF を導入しようとした時の設計に支障が出る
- モバイルからのアクセスは基本的にユーザーエージェントくらいしか判別方法が無い
- Web Viewのユーザーエージェントとも異なってくるので、設定などが諸々面倒
- ライブラリやOSのアップデートによって、フォーマットが意図せず変わる可能性がある
これらはクリティカルな問題にはならないですが、一度リリースした後に設定を変更してしまうとバージョンで差異が生まれることになり、それはそれでさらに面倒なことになります。やるなら初めからやるべきです。
ユーザーエージェント設計のベストプラクティス
モバイルアプリのユーザーエージェントは、以下を表現出来ていれば良いと考えられます。
- そのアプリから届いていること
- 何かしらの情報が取れること
- iOS / Android の判別ができること
例えば、以下の形式にします。
${アプリ名}/${アプリバージョン} ${プラットフォーム}/${プラットフォームバージョン}
iOSの実装コード例(Swift)
func userAgent() -> String { let bundleName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName") let bundleVersion = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleVersion") let systemName = UIDevice.currentDevice().systemName let systemVersion = UIDevice.currentDevice().systemVersion return "\(bundleName)/\(bundleVersion) \(systemName)/\(systemVersion)" }
Androidの実装コード例
public String getUserAgent() { return "AppName/" + BuildConfig.VERSION_NAME + " Android/" + Build.VERSION.RELEASE; }
Web Viewのユーザーエージェントについて
Web Viewのユーザーエージェントはデフォルトのままの方が良いです。
- Androidの場合はChrome Custom Tabがデファクトになりつつあるが、Chrome Custom Tabを使っている場合はユーザーエージェントを変更できない
- iOSの場合はSafari View Controllerがデファクトになりつつあるが、Safari View Controllerを使っている場合はユーザーエージェントを変更できない
- Webページによってはユーザーエージェントによって処理を変えている可能性があるため、変に弄らない方が良い
まとめ
- APIへのリクエスト時にはユーザーエージェントを
${アプリ名}/${アプリバージョン} ${プラットフォーム}/${プラットフォームバージョン}
のような形式にする - Web Viewのユーザーエージェントは変更しない
余談
リクエスト元の種類を判別するためにはユーザーエージェントで判別する方法がスタンダードな方法ですが、別途カスタムリクエストヘッダーを用意する方法もあります。しかしながらリクエストのデータ量を増やすことになるので、微量ですがレイテンシに影響が出るはずなので、要件と照らし合わせて何が最もベストな解か考えるようにしましょう。